// 
// Entify project
//  
// Author:
//       Lauri Taimila <lauri@taimila.com>
// 
// Copyright (c) 2010 Lauri Taimila
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using System;
using System.IO;
using System.Linq;
using System.Transactions;
using System.Collections;
using System.Collections.Generic;

using Taimila.Entify.Sqlite;

namespace Taimila.Entify
{
	/// <summary>
	/// Entity context is container for application domain entities.
	/// </summary>
	public sealed class EntityContext
	{	
		/// <summary>
		/// Lock object used by EntityContext public interface.
		/// </summary>
		private readonly object entityContextLock = new object();
		
		/// <summary>
		/// Settings for the library.
		/// </summary>
		private readonly EntityContextSettings settings;
		
		/// <value>
		/// Persistent store.
		/// </value>
		private IPersistentStore persistentStore;
		
		/// <summary>
		/// Type coordinator.
		/// </summary>
		private ITypeCoordinator typeCoordinator;

		/// <summary>
		/// Creates a new entity context with given settings.
		/// </summary>
		/// <param name="contextSettings">
		/// Settings for context. <see cref="EntityContextSettings"/>
		/// </param>
		internal EntityContext(EntityContextSettings contextSettings)
		{
			this.settings = contextSettings;
			this.InitializeContext();
		}
		
		/// <summary>
		/// Add new entities to entity context.
		/// </summary>
		/// <param name="entities">
		/// Entities to be added.
		/// </param>
		public void Add<T>(params T[] entities) where T : Entity
		{
			this.CheckCanEntitiesBeProcessed(entities);
			
			if(this.ContainsAlreadyAddedEntities(entities))
			{
				throw new AlreadyExistsException("One or more of the specified entities already exist in the entity context.");
			}
			
			lock(this.entityContextLock)
			{	
				this.PreparePersistentStoreForType(typeof(T));				
				this.persistentStore.Add(entities);
				this.SetSouceContext(entities);
			}
		}
		
		/// <summary>
		/// Update given existing entities in the entity context.
		/// </summary>
		/// <param name="entities">
		/// Entities to be updated. 
		/// </param>
		public void Update<T>(params T[] entities) where T : Entity
		{
			this.CheckCanEntitiesBeProcessed(entities);
			
			lock(this.entityContextLock)
			{
				if(!this.typeCoordinator.IsKnownType(typeof(T)))
				{
					throw new EntityNotFoundException("None of the given entities exist in this entity context.");	
				}
				
				this.persistentStore.Update(entities);
			}
		}
		
		/// <summary>
		/// Remove given existing entities from the entity context.
		/// </summary>
		/// <param name="entities">
		/// Entities to be removed. 
		/// </param>
		public void Remove<T>(params T[] entities) where T : Entity
		{
			this.CheckCanEntitiesBeProcessed(entities);
			
			lock(this.entityContextLock)
			{
				if(!this.typeCoordinator.IsKnownType(typeof(T)))
				{
					throw new EntityNotFoundException("None of the given entities exist in this entity context.");	
				}
				
				this.persistentStore.Remove(entities);
				this.RemoveEntityMetadata(entities);
			}
		}
		
		/// <summary>
		/// Get all stored entities of given type.
		/// </summary>
		/// <returns>
		/// Entities. 
		/// </returns>
		public T[] GetAll<T>() where T : Entity
		{
			lock(this.entityContextLock)
			{
				if(!this.typeCoordinator.IsKnownType(typeof(T)))
				{
					return new T[0];
				}
				
				T[] entities = this.GetEntities(new EntityRequest<T>());
				this.SetSouceContext(entities);
				return entities;
			}
		}
		
		/// <summary>
		/// Get entities with entity request. Returns all entities
		/// that match the request.
		/// </summary>
		/// <param name="entityRequest">
		/// Entity request. <see cref="EntityRequest<T>"/>
		/// </param>
		/// <returns>
		/// All found entities. 
		/// </returns>
		public T[] GetEntities<T>(EntityRequest<T> entityRequest) where T : Entity
		{
			lock(this.entityContextLock)
			{
				if(!this.typeCoordinator.IsKnownType(typeof(T)))
				{
					return new T[0];
				}
				
				T[] entities = this.persistentStore.Get(entityRequest);
				this.SetSouceContext(entities);
				return entities;
			}
		}
		
		/// <summary>
		/// Get entity count of given entity type.
		/// </summary>
		/// <returns>
		/// Number of entities in entity context. <see cref="System.Int64"/>
		/// </returns>
		public long GetEntityCount<T>() where T : Entity
		{
			lock(this.entityContextLock)
			{
				if(this.typeCoordinator.IsKnownType(typeof(T)))
				{
					return this.persistentStore.GetEntityCount<T>();
				}
				else
				{
					return 0;	
				}
			}
		}
		
		/// <summary>
		/// Takes snapshot of the current state of the entity context. Returns
		/// snapshot identifier that can be used later to revert context into
		/// current state. Snapshots cannot be made from in-memory entity context.
		/// </summary>
		/// <exception cref="SnapshotException">Exception is thrown if snapshot cannot be created.</exception>
		/// <returns>
		/// Snapshot identifier. <see cref="System.Int64"/>
		/// </returns>
		public long TakeSnapshot()
		{
			if(this.settings.InMemory)
			{
				throw new SnapshotException("Snapshots cannot be made from in-memory entity context");
			}
			
			lock(this.entityContextLock)
			{
				long snapshotId = DateTime.Now.ToFileTime();
				
				try
				{
					File.Copy(this.settings.PersistentStoreLocation, this.GetFilenameForSnapshot(snapshotId));
				}
				catch(Exception e)
				{
					throw new SnapshotException("Unable to take snapshot from entity context.", e);
				}
				
				return snapshotId;
			}
		}
		
		/// <summary>
		/// Reverts entity context back to the state of given snapshot.
		/// </summary>
		/// <param name="snapshotId">
		/// Snapshot identifier. <see cref="System.Int64"/>
		/// </param>
		public void RevertToSnapshot(long snapshotId)
		{
			lock(this.entityContextLock)
			{
				if(this.SnapshotExists(snapshotId))
				{
					string tempExtension = ".temp." + DateTime.Now.ToFileTime();
					try
					{
						this.FinalizeContext();
						File.Move(this.settings.PersistentStoreLocation, this.settings.PersistentStoreLocation + tempExtension);
						File.Copy(this.GetFilenameForSnapshot(snapshotId), this.settings.PersistentStoreLocation);
						File.Delete(this.settings.PersistentStoreLocation + tempExtension);
					}
					catch(Exception e)
					{
						File.Move(this.settings.PersistentStoreLocation + tempExtension, this.settings.PersistentStoreLocation);
						throw new SnapshotException("Revert to snapshot failed!", e);	
					}
					finally
					{
						this.InitializeContext();
					}
				}
				else
				{
					throw new SnapshotException("Snapshot doesn't exists for given identifier.");	
				}
			}
		}
		
		/// <summary>
		/// Removes snapshot by snapshot identifier.
		/// </summary>
		/// <param name="snapshotId">
		/// Snapshot identifier. <see cref="System.Int64"/>
		/// </param>
		public void RemoveSnapshot(long snapshotId)
		{
			if(this.SnapshotExists(snapshotId))
			{
				try
				{
					File.Delete(this.GetFilenameForSnapshot(snapshotId));
				}
				catch(Exception e)
				{
					throw new SnapshotException("Unable to remove snapshot '" + snapshotId + "'", e);	
				}
			}
			else
			{
				throw new SnapshotException("Snapshot doesn't exists for given identifier.");
			}
		}
		
		/// <summary>
		/// Initialized context by creating persistent store and type coordinator.
		/// </summary>
		private void InitializeContext()
		{
			IPersistentFactory factory = this.CreatePersistentFactory();
			this.typeCoordinator = factory.CreateTypeCoordinator();
			this.persistentStore = factory.CreatePersistentStore();
		}
		
		/// <summary>
		/// Finalizes context by closing persistent store and type coordinator.
		/// </summary>
		private void FinalizeContext()
		{
			this.typeCoordinator = null;
			this.persistentStore = null;
		}
		
		/// <summary>
		/// Create filename for given snapshot identifier.
		/// </summary>
		/// <param name="snapshotId">
		/// Snapshot identifier. <see cref="System.Int64"/>
		/// </param>
		/// <returns>
		/// Filename of the snapshot. <see cref="System.String"/>
		/// </returns>
		private string GetFilenameForSnapshot(long snapshotId)
		{
			return this.settings.PersistentStoreLocation + ".snapshot." + snapshotId;
		}
		
		/// <summary>
		/// Check if snapshot exists for given snapshot identifier.
		/// </summary>
		/// <param name="snapshotId">
		/// Snapshot identifier. <see cref="System.Int64"/>
		/// </param>
		/// <returns>
		/// True if snapshot exists, otherwise false. <see cref="System.Boolean"/>
		/// </returns>
		private bool SnapshotExists(long snapshotId)
		{
			return File.Exists(this.GetFilenameForSnapshot(snapshotId));
		}
				
		/// <summary>
		/// Prepares persistent store for given type. If given type has relations
		/// to other entity types, those are added recursively.
		/// </summary>
		/// <param name="type">
		/// Type to prepare for. <see cref="Type"/>
		/// </param>
		private void PreparePersistentStoreForType(Type type)
		{
			if(!EntityValidator.IsValidEntityType(type))
			{
				throw new InvalidEntityTypeException();
			}
			
			if(!this.typeCoordinator.IsKnownType(type))
			{
				this.typeCoordinator.AddType(type);
				Type[] relatedTypes = AttributeReader.GetRelationTypes(type);
				
				foreach(Type t in relatedTypes)
				{
					if(t.IsGenericType) // Checks is current relation 'To-Many relation'
					{
						this.PreparePersistentStoreForType(t.GetGenericArguments().Single());
					}
					else
					{
						this.PreparePersistentStoreForType(t);
					}
				}

				foreach(var property in type.GetProperties())
				{
					if(AttributeReader.IsRelationship(property))
					{
						Type relationType = AttributeReader.GetRelationType(property);
						this.typeCoordinator.MarkAsRelatedType(relationType, type, property.Name);
					}
				}
			}
		}
		
		/// <summary>
		/// Sets this entity context as source context to given entities.
		/// </summary>
		/// <param name="entities">
		/// Entities which source context is set to this context.
		/// </param>
		private void SetSouceContext<T>(params T[] entities) where T : Entity
		{
			foreach(var entity in entities)
			{
				entity.SetSourceContext(this);
			}
		}

		/// <summary>
		/// Removes entity metadata from given entities.
		/// </summary>
		/// <param name="entities">
		/// Entities which metadata is removed.
		/// </param>
		private void RemoveEntityMetadata<T>(params T[] entities) where T : Entity
		{
			foreach(var entity in entities)
			{
				entity.SetSourceContext(null);
				entity.SetEntityId(null);
				entity.SetLastModifiedDate(null);
			}
		}
		
		/// <summary>
		/// Create persistent factory. Actual factory is created depending
		/// on context settings.
		/// </summary>
		/// <returns>
		/// Persistent factory. <see cref="IPersistentFactory"/>
		/// </returns>
		private IPersistentFactory CreatePersistentFactory()
		{
			if(this.settings.InMemory)
			{
				return new SqliteFactory("URI=file::memory:,version=3");
			}
			else
			{
				return new SqliteFactory("URI=file:" + this.settings.PersistentStoreLocation + ",version=3");
			}
		}
		
		/// <summary>
		/// Checks that entities can be processed. This means that entities parameter
		/// is not null or empty array. And if context enforces validation then all
		/// entities must be valid to be processed.
		/// Throws exception if entities are not ready to be processed.
		/// </summary>
		/// <param name="entities">
		/// Entities to be checked. 
		/// </param>
		private void CheckCanEntitiesBeProcessed<T>(T[] entities) where T : Entity
		{
			if(entities == null)
			{
				throw new ArgumentNullException();	
			}
			
			if(entities.Length == 0)
			{
				return;	
			}
			
			foreach(T entity in entities)
			{
				if(this.HasEntityNonAddedEntitiesAsRelations(entity))
				{
					throw new ArgumentException("One or more of the specified entities contained unadded entities as relations.");	
				}
			}
			
			if(this.settings.EnforceValidationRules)
			{
				ValidationResults results = EntityValidator.Validate(entities);
				if(!results.IsValid)
				{
					throw new ValidationException(results, "Specified entities were not valid. None of the entities were processed by context.");
				}
			}
		}
		
		/// <summary>
		/// Checks does specified entity have relations to entities that are not added
		/// to the current context.
		/// </summary>
		/// <param name="entity">
		/// Entity to be checked. <see cref="Entity"/>
		/// </param>
		/// <returns>
		/// True if entity contains relations to unadded entities, otherwise false. <see cref="System.Boolean"/>
		/// </returns>
		private bool HasEntityNonAddedEntitiesAsRelations<T>(T entity) where T : Entity
		{
			foreach(var property in typeof(T).GetProperties())
			{
				if(AttributeReader.IsManyRelationship(property))
				{
					List<Entity> relatedEntities = entity.GetLoadedRelatedEntities(property);
					
					foreach(Entity relatedEntity in relatedEntities)
					{
						if(relatedEntity != null && !relatedEntity.IsInEntityContext())
						{
							return true;	
						}
					}
				}
				else if(AttributeReader.IsRelationship(property))
				{
					Entity relatedEntity = entity.GetLoadedRelatedEntity(property);
					
					if(relatedEntity != null && !relatedEntity.IsInEntityContext())
					{
						return true;
					}
				}
			}
			
			return false;
		}
		
		/// <summary>
		/// Checks does any of the specified entities already belong to any context.
		/// </summary>
		/// <param name="entities">
		/// Entities to be checked. 
		/// </param>
		/// <returns>
		/// True if there is one or more entities that alrady belong to some context, otherwise false. <see cref="System.Boolean"/>
		/// </returns>
		private bool ContainsAlreadyAddedEntities<T>(T[] entities) where T : Entity
		{
			foreach(Entity entity in entities)
			{
				if(entity.IsInEntityContext())
				{
					return true;
				}
			}
			
			return false;
		}
	}
}
